package gdma.hqdes.article.renderer;

import com.deepoove.poi.XWPFTemplate;
import com.deepoove.poi.config.Configure;
import com.deepoove.poi.config.ConfigureBuilder;
import com.deepoove.poi.data.ChartMultiSeriesRenderData;
import com.deepoove.poi.data.PictureRenderData;
import com.deepoove.poi.data.PictureType;
import com.deepoove.poi.data.Pictures;
import com.deepoove.poi.template.MetaTemplate;
import common.util.TreeUtils;
import gdma.hqdes.article.bean.RenderContext;
import gdma.hqdes.article.policy.UnitScoreTablePolicy;
import gdma.hqdes.article.util.TemplateUtils;
import gdma.hqdes.bean.HqdIndex;
import gdma.hqdes.bean.HqdProject;
import gdma.hqdes.bean.Unit;
import gdma.hqdes.bean.UnitScore;
import gdma.hqdes.service.IndexService;
import gdma.hqdes.service.ProjectService;
import gdma.hqdes.service.UnitService;
import gdma.hqdes.stat.bean.StatQuery;
import gdma.hqdes.stat.dao.StatQueryMapper;
import gdma.hqdes.stat.renderer.GeoMapRenderer;
import gdma.hqdes.stat.renderer.UnitScoreDataRenderer;
import io.vavr.Tuple;
import lombok.AllArgsConstructor;
import lombok.Data;
import org.apache.commons.lang3.RandomUtils;
import org.apache.poi.xwpf.usermodel.XWPFChart;
import org.apache.poi.xwpf.usermodel.XWPFPicture;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.io.*;
import java.util.*;
import java.util.function.Predicate;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import java.util.stream.IntStream;

import static gdma.hqdes.stat.renderer.GeoMapRenderer.DEFAULT_COLOR_MAPPER;
import static gdma.hqdes.stat.renderer.UnitScoreDataRenderer.adjustComboTypes;
import static gdma.hqdes.stat.renderer.UnitScoreDataRenderer.convertToMap;

/**
 * @Author wangyue
 * @Version 3.0
 * @Date 2024-08-28
 */
@Service
public class TemplateRenderer {

    public static final File APP_PATH = new File(".");

    Pattern DATA_KEY_PATTERN = Pattern.compile("\\{\\{([^\\}]+)\\}\\}");

    @Autowired
    ProjectService projectService;

    @Autowired
    IndexService indexService;

    @Autowired
    UnitService unitService;

    @Autowired
    StatQueryMapper statQueryMapper;

    @Autowired
    UnitScoreDataRenderer unitScoreDataRenderer;

    @Autowired
    GeoMapRenderer geoMapRenderer;

    public InputStream render(File templateFile, RenderContext context) {
        //提取模板文件中定义的占位符（请仔细检查嵌入字符串是否含有尾端空格，pol-tl可不会帮你trim）
        ConfigureBuilder builder = Configure.builder().buildGrammerRegex("[^\\}]+").useSpringEL();
        List<MetaTemplate> metaTemplates;
        try(InputStream in = new FileInputStream(templateFile);
            XWPFTemplate template = XWPFTemplate.compile(in, builder.build())) {
            metaTemplates = template.getElementTemplates();
        } catch(IOException e) {
            throw new RuntimeException(e);
        }

        //提取全部数据键值
        List<String> dataKeys = metaTemplates.stream()
                .map(v -> {
                    String s = v.toString();
                    Matcher m = DATA_KEY_PATTERN.matcher(s);
                    return m.find() ? m.group(1) : s;
                }).collect(Collectors.toList());

        //表格元素需要绑定渲染策略
        for(String dataKey : dataKeys) {
            if(dataKey.equals("tableIndexScoresLevel2")) {
                builder.bind(dataKey, new UnitScoreTablePolicy(2));
            } else if(dataKey.equals("tableIndexScoresLevel3")) {
                builder.bind(dataKey, new UnitScoreTablePolicy(3));
            }
        }

        try(InputStream in = new FileInputStream(templateFile);
            XWPFTemplate template = XWPFTemplate.compile(in, builder.build());
            ByteArrayOutputStream out = new ByteArrayOutputStream()) {

            HqdProject project = context.getProject();
            if(project == null) throw new IllegalStateException("渲染出错，未指定项目。");
            Map<String, Object> renderData = new HashMap<>();
            renderData.put("Project", project);
            renderData.put("Unit", context.getUnit());
            for(String dataKey : dataKeys) {
                if(dataKey.contains("Unit.") && renderData.get("Unit") == null) {
                    Unit sampleUnit = unitService.getUnits(project.getScope()).get(0);
                    renderData.put("Unit", sampleUnit);
                }
                if(dataKey.contains("RootIndex.") && renderData.get("RootIndex") == null) {
                    Unit unit = (Unit)renderData.get("Unit");
                    HqdIndex root = unit != null ?
                            indexService.buildScoredIndexTree(project.getId(), unit.getId()) :
                            indexService.buildIndexTree(project.getIndexVerId());
                    renderData.put("RootIndex", root);
                }
                if(dataKey.startsWith("tableIndexScores")) {
                    HqdIndex root = (HqdIndex)renderData.get("RootIndex");
                    renderData.put(dataKey, root != null ? root.getChildren() : new ArrayList<>());
                }
                if(dataKey.contains("template.") && renderData.get("template") == null) {
                    renderData.put("template", new TemplateAgent(template));
                }
                if(dataKey.contains("overall.") && renderData.get("overall") == null) {
                    renderData.put("overall", new OverallAnalysis(context.getProject(), "广东省"));
                }
                if(dataKey.contains("municipals.") && renderData.get("overall") == null) {
                    renderData.put("municipals", new GdMunicipalAnalysis(context.getProject(), 90));
                }
            }
            template.render(renderData, out);
            return new ByteArrayInputStream(out.toByteArray());
        } catch(Exception e) {
            throw new RuntimeException(e);
        }
    }

    class TemplateAgent {

        private XWPFTemplate template;

        public TemplateAgent(XWPFTemplate template) {
            this.template = template;
        }

        public PictureRenderData addPicture(int statQueryId)  {
            StatQuery query = statQueryMapper.selectById(statQueryId);
            Long parentUnitId = query.getUnitId();
            Unit parentUnit = unitService.getUnit(parentUnitId);
            ChartMultiSeriesRenderData renderData = unitScoreDataRenderer.renderHeatmap(query);
            int width = 800, height = 600;
            //甚至可以匹配模板中设置的图片宽高
            XWPFPicture placeholder = TemplateUtils.findPlaceheldPicture(template, s -> s.contains("addPicture(" + statQueryId + ")"));
            if(placeholder != null) {
                width = TemplateUtils.getPictureWidthInPixels(placeholder);
                height = TemplateUtils.getPictureHeightInPixels(placeholder);
            }
            try(InputStream input = geoMapRenderer.renderImage(parentUnit.getShortName(), DEFAULT_COLOR_MAPPER, convertToMap(renderData), 0, width, height)) {
                PictureRenderData pictureRenderData = Pictures.ofStream(input, PictureType.PNG).size(width, height).create();
                return pictureRenderData;
            } catch(IOException e) {
                throw new RuntimeException(e);
            }
        }

        public ChartMultiSeriesRenderData addChart(int statQueryId)  {
            StatQuery query = statQueryMapper.selectById(statQueryId);
            ChartMultiSeriesRenderData renderData = unitScoreDataRenderer.renderIndexCombos(query);
            //甚至可以按模板中的设置调整显示样式
            XWPFChart placeholder = TemplateUtils.findPlaceheldChart(template, s -> s.contains("addChart(" + statQueryId + ")"));
            if(placeholder != null) adjustComboTypes(renderData, placeholder);
            return renderData;
        }

        public ChartMultiSeriesRenderData addChart2(int statQueryId)  {
            StatQuery query = statQueryMapper.selectById(statQueryId);
            ChartMultiSeriesRenderData renderData = unitScoreDataRenderer.renderHistoryIndexCombos(query);
            //甚至可以按模板中的设置调整显示样式
            XWPFChart placeholder = TemplateUtils.findPlaceheldChart(template, s -> s.contains("addChart2(" + statQueryId + ")"));
            if(placeholder != null) adjustComboTypes(renderData, placeholder);
            return renderData;
        }

    }

    public static double getScore(HqdIndex index) {
        if(index != null) {
            UnitScore score = index.getUnitScore();
            if(score != null)
                return score.getScore();
        }
        return 0.f;
    }

    @Data
    @AllArgsConstructor
    static class NameAndValue<T extends Comparable> {
        String name;
        T value;
    }

    class OverallAnalysis extends HashMap<String, Object> {

        public OverallAnalysis(HqdProject prj, String shortName) {
            //当前年份
            HqdProject project = projectService.getProject(prj.getYear(), Unit.SCOPE_PROVINCIAL);
            put("year", project.getYear());

            //全国根指标平均分
            List<Unit> units = unitService.getUnits(Unit.SCOPE_PROVINCIAL);
            Map<String, HqdIndex> unitScoreTrees = units.stream()
                    .collect(Collectors.toMap(Unit::getShortName, unit -> indexService.buildScoredIndexTree(project.getId(), unit.getId())));
            Map<String, Double> rootIndexScores = unitScoreTrees.entrySet().stream()
                    .collect(Collectors.toMap(Entry::getKey, e -> getScore(e.getValue())));
            double rootIndexScoreAvg = rootIndexScores.entrySet().stream().collect(Collectors.averagingDouble(Entry::getValue));
            put("rootIndexScoreAvg", String.format("%.2f", rootIndexScoreAvg));

            //目标省根指标得分
            double rootIndexScore = rootIndexScores.get(shortName);
            put("rootIndexScore", String.format("%.2f", rootIndexScore));

            //目标省根指标得分全国排名
            List<Double> rootIndexTopList = rootIndexScores.values().stream().sorted(Comparator.reverseOrder()).collect(Collectors.toList());
            int rank = IntStream.range(0, rootIndexTopList.size()).filter(i -> rootIndexTopList.get(i) == rootIndexScore).findFirst().orElse(-1);
            put("rootIndexRank", String.format("%d", rank + 1));

            //向前看：目标省低于全国平均分的二级指标项情况
            Map<String, Map<String, Double>> allLevel2IndexScores = unitScoreTrees.entrySet().stream()
                    .collect(Collectors.toMap(Entry::getKey,
                            e -> TreeUtils.findAll(e.getValue(), index -> index.getLevel() == 2).stream()
                                    .collect(Collectors.toMap(HqdIndex::getName, TemplateRenderer::getScore))));
            Map<String, Double> level2IndexScores = allLevel2IndexScores.get(shortName);
            Map<String, Double> allLevel2IndexScoreAvgs = level2IndexScores.keySet().stream()
                    .collect(Collectors.toMap(key -> key, key -> allLevel2IndexScores.values().stream()
                            .map(m -> m.get(key))
                            .collect(Collectors.averagingDouble(f -> f))));
            List<NameAndValue<String>> lowerLevel2Indexes = level2IndexScores.entrySet().stream()
                    .filter(e -> e.getValue() < allLevel2IndexScoreAvgs.get(e.getKey()))
                    .map(e -> new NameAndValue<Double>(e.getKey(), e.getValue() - allLevel2IndexScoreAvgs.get(e.getKey())))
                    .sorted(Comparator.comparing(NameAndValue::getValue))
                    .map(nv -> new NameAndValue<String>(nv.name, String.format("%.2f", nv.value)))
                    .collect(Collectors.toList());
            put("lowerLevel2Indexes", lowerLevel2Indexes);

            //上一年度
            HqdProject project0 = projectService.getProject(prj.getYear() - 1, Unit.SCOPE_PROVINCIAL);
            put("canContrast", project0 != null);
            if(project0 != null) {
                put("contrastYear", project0.getYear());

                //上一年度目标省根指标增长/下降情况
                Map<String, HqdIndex> unitScoreTrees0 = units.stream()
                        .collect(Collectors.toMap(Unit::getShortName, unit -> indexService.buildScoredIndexTree(project0.getId(), unit.getId())));
                Map<String, Double> rootIndexScores0 = unitScoreTrees0.entrySet().stream()
                        .collect(Collectors.toMap(Entry::getKey, e -> getScore(e.getValue())));
                //上一年度目标省根指标得分
                double rootIndexContrastScore = rootIndexScores0.get(shortName);
                put("rootIndexContrastScore", String.format("%.2f", rootIndexContrastScore));
                boolean rootIndexIncreased = rootIndexScore >= rootIndexContrastScore;
                put("rootIndexIncreased", rootIndexIncreased);
                put("rootIndexIncrementAbs", String.format("%.2f", Math.abs(rootIndexScore - rootIndexContrastScore)));
                if(rootIndexIncreased) {
                    put("rootIndexIncRate", String.format("%.2f", (rootIndexScore - rootIndexContrastScore) / rootIndexContrastScore * 100));
                    put("rootIndexIncRateRank", String.format("%d", 1));
                }

                //向后看：目标省一、二级指标得分、全国排名本年度与上一年度对比情况
                Map<String, Integer> level1IndexRanks = calcUnitIndexRanks(unitScoreTrees, i -> i.getLevel() == 1, shortName);
                Map<String, Integer> level1IndexContrastRanks = calcUnitIndexRanks(unitScoreTrees0, i -> i.getLevel() == 1, shortName);

                List<NameAndValue<Integer>> level1IndexRankList = level1IndexRanks.entrySet().stream()
                        .map(e -> new NameAndValue<Integer>(e.getKey(), level1IndexContrastRanks.getOrDefault(e.getKey(), level1IndexContrastRanks.size() + 1) - e.getValue()))
                        .collect(Collectors.toList());
                List<NameAndValue<String>> level1IndexRankUpList = level1IndexRankList.stream().filter(nv -> nv.value > 1)
                        .map(nv -> new NameAndValue<String>(nv.name, "+" + String.format("%d", nv.value)))
                        .collect(Collectors.toList());
                put("level1IndexRankUpList", level1IndexRankUpList);
                List<NameAndValue<String>> level1IndexRankKeepList = level1IndexRankList.stream().filter(nv -> nv.value >= -1 && nv.value <= 1)
                        .map(nv -> new NameAndValue<String>(nv.name, (nv.value >= 0 ? "+" : "") + String.format("%d", nv.value)))
                        .collect(Collectors.toList());
                put("level1IndexRankKeepList", level1IndexRankKeepList);
                List<NameAndValue<Integer>> level1IndexRankDownList = level1IndexRankList.stream().filter(nv -> nv.value < -1).collect(Collectors.toList());
                put("level1IndexRankDownList", level1IndexRankDownList);

                Map<String, Integer> level2IndexRanks = calcUnitIndexRanks(unitScoreTrees, i -> i.getLevel() == 2, shortName);
                Map<String, Integer> level2IndexContrastRanks = calcUnitIndexRanks(unitScoreTrees0, i -> i.getLevel() == 2, shortName);

                List<NameAndValue<Integer>> level2IndexRankDownList = level2IndexRanks.entrySet().stream()
                        .filter(e -> level2IndexContrastRanks.getOrDefault(e.getKey(), level2IndexContrastRanks.size() + 1) < e.getValue())
                        .map(e -> new NameAndValue<Integer>(e.getKey(), e.getValue()))
                        .collect(Collectors.toList());
                put("level2IndexRankDownList", level2IndexRankDownList);
            }
        }

        private void randomizeScore(Map<String, HqdIndex> unitScoreTrees) {
            for(HqdIndex tree : unitScoreTrees.values()) {
                TreeUtils.traverse(tree, index -> {
                    UnitScore score = index.getUnitScore();
                    if(score == null) {
                        score = new UnitScore();
                        index.setUnitScore(score);
                    }
                    float base = RandomUtils.nextFloat(0.f, 1.f) < 0.1f ? 0.f : 50.f;
                    score.setScore(base + RandomUtils.nextFloat(0.f, 50.f));
                });
            }
        }

        private Map<String, Integer> calcUnitIndexRanks(Map<String, HqdIndex> unitScoreTrees, Predicate<HqdIndex> filter, String gdShortName) {
            Map<String, Map<String, Double>> allIndexScores = unitScoreTrees.entrySet().stream()
                    .collect(Collectors.toMap(Entry::getKey,
                            e -> TreeUtils.findAll(e.getValue(), filter).stream()
                                    .collect(Collectors.toMap(HqdIndex::getName, TemplateRenderer::getScore))));
            Map<String, List<Double>> allIndexTopLists = allIndexScores.values().stream()
                    .flatMap(map -> map.entrySet().stream())
                    .collect(Collectors.groupingBy(Entry::getKey, Collectors.mapping(Entry::getValue,
                            Collectors.collectingAndThen(Collectors.toList(), list -> {
                                Collections.sort(list);
                                return list;
                            }))));
            Map<String, Double> gdIndexScores = allIndexScores.get(gdShortName);
            Map<String, Integer> gdIndexRanks = gdIndexScores.entrySet().stream()
                    .collect(Collectors.toMap(Entry::getKey, e -> {
                        List<Double> topList = allIndexTopLists.get((e.getKey()));
                        int rank = IntStream.range(0, topList.size()).filter(i -> topList.get(i) == e.getValue()).findFirst().orElse(-1);
                        return rank + 1;
                    }));
            return gdIndexRanks;
        }

    }

    class GdMunicipalAnalysis extends HashMap<String, Object> {
        public GdMunicipalAnalysis(HqdProject prj, float zAbove) {
            HqdProject project = projectService.getProject(prj.getYear(), Unit.SCOPE_MUNICIPAL);
            put("year", project.getYear());

            //根指标平均分
            List<Unit> units = unitService.getUnits(Unit.SCOPE_MUNICIPAL);
            Map<String, HqdIndex> unitScoreTrees = units.stream()
                    .collect(Collectors.toMap(Unit::getShortName, unit -> indexService.buildScoredIndexTree(project.getId(), unit.getId())));
            Map<String, Double> rootIndexScores = unitScoreTrees.entrySet().stream()
                    .collect(Collectors.toMap(Entry::getKey, e -> getScore(e.getValue())));
            double rootIndexScoreAvg = rootIndexScores.entrySet().stream().collect(Collectors.averagingDouble(Entry::getValue));
            put("rootIndexScoreAvg", String.format("%.2f", rootIndexScoreAvg));

            int year0 = prj.getYear() - 1;
            put("year0", year0);
            HqdProject project0 = projectService.getProject(year0, Unit.SCOPE_MUNICIPAL);
            Map<String, HqdIndex> unitScoreTrees0;
            Map<String, Double> rootIndexScores0;
            if(project0 != null) {
                unitScoreTrees0 = units.stream()
                        .collect(Collectors.toMap(Unit::getShortName, unit -> indexService.buildScoredIndexTree(project0.getId(), unit.getId())));
                rootIndexScores0 = unitScoreTrees0.entrySet().stream()
                        .collect(Collectors.toMap(Entry::getKey, e -> getScore(e.getValue())));
            } else {
                unitScoreTrees0 = unitScoreTrees.entrySet().stream()
                        .collect(Collectors.toMap(e -> e.getKey(), e -> TreeUtils.cloneTree(e.getValue(), index0 -> {
                            HqdIndex index = new HqdIndex();
                            BeanUtils.copyProperties(index0, index);
                            index.setUnitScore(null);
                            return index;
                        })));
                rootIndexScores0 = unitScoreTrees0.entrySet().stream()
                        .collect(Collectors.toMap(Entry::getKey, e -> getScore(e.getValue())));
            }
            double rootIndexScoreAvg0 = rootIndexScores0.entrySet().stream().collect(Collectors.averagingDouble(Entry::getValue));
            put("rootIndexScoreAvg0", String.format("%.2f", rootIndexScoreAvg0));
            put("rootIndexScoreAvgDiff", String.format("%.2f", rootIndexScoreAvg - rootIndexScoreAvg0));

            List<Unit> zUnits = units.stream().filter(u -> "Z".equals(u.getAreaTag())).collect(Collectors.toList());
            List<String> zAboveList = zUnits.stream()
                    .map(Unit::getShortName)
                    .filter(s -> {
                        HqdIndex index = unitScoreTrees.get(s);
                        return getScore(index) >= zAbove;
                    }).collect(Collectors.toList());
            put("zAboveList", zAboveList);
            put("zAbovePercentage", String.format("%.2f", zAboveList.size() * 100f / zUnits.size()));
            put("zAbove", zAbove);

            List<Unit> yUnits = units.stream().filter(u -> "Y".equals(u.getAreaTag())).collect(Collectors.toList());
            List<String> yAboveList = yUnits.stream()
                    .map(Unit::getShortName)
                    .filter(s -> getScore(unitScoreTrees.get(s)) >= rootIndexScoreAvg)
                    .collect(Collectors.toList());
            put("yAboveList", yAboveList);
            int yUnitNum = (int)units.stream().filter(u -> "Y".equals(u.getAreaTag())).count();

            float[] yBelowRange = {Float.MAX_VALUE, 0};
            List<String> yBelowList = yUnits.stream()
                    .map(Unit::getShortName)
                    .filter(s -> {
                        HqdIndex index = unitScoreTrees.get(s);
                        float score = (float)getScore(index);
                        boolean below = score < rootIndexScoreAvg;
                        if(below) {
                            if(score < yBelowRange[0]) yBelowRange[0] = score;
                            if(score > yBelowRange[1]) yBelowRange[1] = score;
                        }
                        return below;
                    }).collect(Collectors.toList());
            put("yBelowList", yBelowList);
            put("yBelowPercentage", String.format("%.2f", yBelowList.size() * 100f / yUnitNum));
            put("yBelowMin", String.format("%.2f", yBelowRange[0]));
            put("yBelowMax", String.format("%.2f", yBelowRange[1]));

            Map<String, Double> zRootIndexScores = zUnits.stream()
                    .map(u -> Tuple.of(u.getShortName(), unitScoreTrees.get(u.getShortName())))
                    .collect(Collectors.toMap(t -> t._1, t -> getScore(t._2)));
            double zRootIndexScoreAvg = zRootIndexScores.entrySet().stream().collect(Collectors.averagingDouble(Entry::getValue));
            put("zRootIndexScoreAvg", String.format("%.2f", zRootIndexScoreAvg));
            Map<String, Double> zRootIndexScores0 = zUnits.stream()
                    .map(u -> Tuple.of(u.getShortName(), unitScoreTrees0.get(u.getShortName())))
                    .collect(Collectors.toMap(t -> t._1, t -> getScore(t._2)));
            double zRootIndexScoreAvg0 = zRootIndexScores0.entrySet().stream().collect(Collectors.averagingDouble(Entry::getValue));
            put("zRootIndexScoreAvg0", String.format("%.2f", zRootIndexScoreAvg0));
            put("zRootIndexScoreAvgInc", String.format("%.2f", (rootIndexScoreAvg - rootIndexScoreAvg0) * 100f / rootIndexScoreAvg0));
            List<String> zAboveList2 = zUnits.stream()
                    .map(Unit::getShortName)
                    .filter(s -> getScore(unitScoreTrees.get(s)) >= zRootIndexScoreAvg)
                    .collect(Collectors.toList());
            put("zAboveList2", zAboveList2);
            List<String> zBelowList2 = zUnits.stream()
                    .map(Unit::getShortName)
                    .filter(s -> getScore(unitScoreTrees.get(s)) < zRootIndexScoreAvg)
                    .collect(Collectors.toList());
            put("zBelowList2", zBelowList2);
            List<String> zBelowList = zUnits.stream()
                    .map(Unit::getShortName)
                    .filter(s -> getScore(unitScoreTrees.get(s)) < rootIndexScoreAvg)
                    .collect(Collectors.toList());
            put("zBelowList", zBelowList);

            Map<String, Double> yRootIndexScores = yUnits.stream()
                    .map(u -> Tuple.of(u.getShortName(), unitScoreTrees.get(u.getShortName())))
                    .collect(Collectors.toMap(t -> t._1, t -> getScore(t._2)));
            double yRootIndexScoreAvg = yRootIndexScores.entrySet().stream().collect(Collectors.averagingDouble(Entry::getValue));
            put("yRootIndexScoreAvg", String.format("%.2f", yRootIndexScoreAvg));
            Map<String, Double> yRootIndexScores0 = yUnits.stream()
                    .map(u -> Tuple.of(u.getShortName(), unitScoreTrees0.get(u.getShortName())))
                    .collect(Collectors.toMap(t -> t._1, t -> getScore(t._2)));
            double yRootIndexScoreAvg0 = yRootIndexScores0.entrySet().stream().collect(Collectors.averagingDouble(Entry::getValue));
            put("yRootIndexScoreAvg0", String.format("%.2f", yRootIndexScoreAvg0));
            put("yRootIndexScoreAvgInc", String.format("%.2f", (yRootIndexScoreAvg - yRootIndexScoreAvg0) * 100f / yRootIndexScoreAvg0));
            put("zyRootIndexScoreAvgDiff", String.format("%.2f", zRootIndexScoreAvg - yRootIndexScoreAvg));
            List<String> yAboveList2 = yUnits.stream()
                    .map(Unit::getShortName)
                    .filter(s -> getScore(unitScoreTrees.get(s)) >= yRootIndexScoreAvg)
                    .collect(Collectors.toList());
            put("yAboveList2", yAboveList2);
            List<String> yBelowList2 = yUnits.stream()
                    .map(Unit::getShortName)
                    .filter(s -> getScore(unitScoreTrees.get(s)) < yRootIndexScoreAvg)
                    .collect(Collectors.toList());
            put("yBelowList2", yBelowList2);
            put("yBelowPercentage2", String.format("%.2f", yBelowList2.size() * 100f / yUnitNum));

            put("zyRootIndexScoreAvgPercentage", String.format("%.2f", (yRootIndexScoreAvg - zRootIndexScoreAvg) * 100f / zRootIndexScoreAvg));
            put("zyRootIndexScoreAvgPercentage0", String.format("%.2f", (yRootIndexScoreAvg0 - zRootIndexScoreAvg0) * 100f / zRootIndexScoreAvg0));
        }
    }

    class CountyAnalysis extends HashMap<String, Object> {
        public CountyAnalysis(HqdProject prj, String shortName) {
            HqdProject project = projectService.getProject(prj.getYear(), Unit.SCOPE_COUNTY);
            put("year", project.getYear());
        }
    }

}